( function () {
/**
 * GCodeLoader is used to load gcode files usually used for 3D printing or CNC applications.
 *
 * Gcode files are composed by commands used by machines to create objects.
 *
 * @class GCodeLoader
 * @param {Manager} manager Loading manager.
 */

class GCodeLoader extends THREE.Loader {
  constructor(manager) {
    super(manager);
    this.splitLayer = false;
  }

  load(url, onLoad, onProgress, onError) {
    const scope = this;
    const loader = new THREE.FileLoader(scope.manager);
    loader.setPath(scope.path);
    loader.setRequestHeader(scope.requestHeader);
    loader.setWithCredentials(scope.withCredentials);
    loader.load(url, function (text) {
      try {
        onLoad(scope.parse(text));
      } catch (e) {
        if (onError) {
          onError(e);
        } else {
          console.error(e);
        }

        scope.manager.itemError(url);
      }
    }, onProgress, onError);
  }

  parse(data) {
    let state = {
      x: 0,
      y: 0,
      z: 0,
      e: 0,
      f: 0,
      extruding: false,
      relative: false
    };
    const layers = [];
    let currentLayer = undefined;
    const pathMaterial = new THREE.LineBasicMaterial({
      color: 0xFF0000
    });
    pathMaterial.name = 'path';
    const extrudingMaterial = new THREE.LineBasicMaterial({
      color: 0x00FF00
    });
    extrudingMaterial.name = 'extruded';

    function newLayer(line) {
      currentLayer = {
        vertex: [],
        pathVertex: [],
        z: line.z
      };
      layers.push(currentLayer);
    } //Create lie segment between p1 and p2


    function addSegment(p1, p2) {
      if (currentLayer === undefined) {
        newLayer(p1);
      }

      if (state.extruding) {
        currentLayer.vertex.push(p1.x, p1.y, p1.z);
        currentLayer.vertex.push(p2.x, p2.y, p2.z);
      } else {
        currentLayer.pathVertex.push(p1.x, p1.y, p1.z);
        currentLayer.pathVertex.push(p2.x, p2.y, p2.z);
      }
    }

    function delta(v1, v2) {
      return state.relative ? v2 : v2 - v1;
    }

    function absolute(v1, v2) {
      return state.relative ? v1 + v2 : v2;
    }

    const lines = data.replace(/;.+/g, '').split('\n');

    for (let i = 0; i < lines.length; i++) {
      const tokens = lines[i].split(' ');
      const cmd = tokens[0].toUpperCase(); //Argumments

      const args = {};
      tokens.splice(1).forEach(function (token) {
        if (token[0] !== undefined) {
          const key = token[0].toLowerCase();
          const value = parseFloat(token.substring(1));
          args[key] = value;
        }
      }); //Process commands
      //G0/G1 – Linear Movement

      if (cmd === 'G0' || cmd === 'G1') {
        const line = {
          x: args.x !== undefined ? absolute(state.x, args.x) : state.x,
          y: args.y !== undefined ? absolute(state.y, args.y) : state.y,
          z: args.z !== undefined ? absolute(state.z, args.z) : state.z,
          e: args.e !== undefined ? absolute(state.e, args.e) : state.e,
          f: args.f !== undefined ? absolute(state.f, args.f) : state.f
        }; //Layer change detection is or made by watching Z, it's made by watching when we extrude at a new Z position

        if (delta(state.e, line.e) > 0) {
          line.extruding = delta(state.e, line.e) > 0;

          if (currentLayer == undefined || line.z != currentLayer.z) {
            newLayer(line);
          }
        }

        addSegment(state, line);
        state = line;
      } else if (cmd === 'G2' || cmd === 'G3') {//G2/G3 - Arc Movement ( G2 clock wise and G3 counter clock wise )
        //console.warn( 'THREE.GCodeLoader: Arc command not supported' );
      } else if (cmd === 'G90') {
        //G90: Set to Absolute Positioning
        state.relative = false;
      } else if (cmd === 'G91') {
        //G91: Set to state.relative Positioning
        state.relative = true;
      } else if (cmd === 'G92') {
        //G92: Set Position
        const line = state;
        line.x = args.x !== undefined ? args.x : line.x;
        line.y = args.y !== undefined ? args.y : line.y;
        line.z = args.z !== undefined ? args.z : line.z;
        line.e = args.e !== undefined ? args.e : line.e;
        state = line;
      } else {//console.warn( 'THREE.GCodeLoader: Command not supported:' + cmd );
      }
    }

    function addObject(vertex, extruding, i) {
      const geometry = new THREE.BufferGeometry();
      geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertex, 3));
      const segments = new THREE.LineSegments(geometry, extruding ? extrudingMaterial : pathMaterial);
      segments.name = 'layer' + i;
      object.add(segments);
    }

    const object = new THREE.Group();
    object.name = 'gcode';

    if (this.splitLayer) {
      for (let i = 0; i < layers.length; i++) {
        const layer = layers[i];
        addObject(layer.vertex, true, i);
        addObject(layer.pathVertex, false, i);
      }
    } else {
      const vertex = [],
            pathVertex = [];

      for (let i = 0; i < layers.length; i++) {
        const layer = layers[i];
        const layerVertex = layer.vertex;
        const layerPathVertex = layer.pathVertex;

        for (let j = 0; j < layerVertex.length; j++) {
          vertex.push(layerVertex[j]);
        }

        for (let j = 0; j < layerPathVertex.length; j++) {
          pathVertex.push(layerPathVertex[j]);
        }
      }

      addObject(vertex, true, layers.length);
      addObject(pathVertex, false, layers.length);
    }

    object.quaternion.setFromEuler(new THREE.Euler(-Math.PI / 2, 0, 0));
    return object;
  }

}

THREE.GCodeLoader = GCodeLoader;
} )();
